/*
 *   A task scheduler that implements Monte-Carlo simulation
 *
 *   Copyright (C) 2011 Systasis Computer Systems, Inc.
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *   
 *   $Id: ActivityOnNodeScheduler.java 68 2011-08-01 22:22:17Z jchimene@gmail.com $
 */
package com.systasis.sked.client.presenter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;

import com.google.inject.Singleton;

@Singleton
public class ActivityOnNodeScheduler extends Scheduler {

    // ================================================= mean
    public static double mean(Double[] p) {
	double sum = 0; // sum of all the elements
	for (final Double element : p) {
	    sum += element;
	}
	return sum / p.length;
    }// end method mean

    private boolean allNumbered = false;

    final Logger logger = Logger.getLogger(ActivityOnNodeScheduler.class.getName());

    public Iterable<Activity> activityIterable() {
	if (!allNumbered) {
	    activityNumbering();
	}

	return Activity.asSortedList(source.values());
    }

    /*
     * Activity Numbering Algorithm
     * 
     * Step 1: Give the starting activity number 1.
     * 
     * Step 2: Give the next number to any unnumbered activity whose predecessor
     * activities are each already numbered.
     * 
     * Repeat Step 2 until all activities are numbered.
     */
    public void activityNumbering() {
	int sequenceNumber = 1;

	final Set<Entry<Integer, Activity>> activityList = source.entrySet();

	for (final Entry<Integer, Activity> activity : activityList) {
	    if (0 == activity.getValue().getPredecessorList().size()) {
		activity.getValue().setSequenceNumber(sequenceNumber++);
		break;
	    }
	}

	allNumbered = false;
	while (!allNumbered) {
	    for (final Entry<Integer, Activity> activity : activityList) {
		boolean predNumbered = true;
		for (final Integer predecessor : activity.getValue().getPredecessorList()) {
		    if (null != get(predecessor)) {
			if (null == get(predecessor).getSequenceNumber()) {
			    predNumbered = false;
			}
		    }
		}
		if (predNumbered && (null == source.get(activity.getKey()).getSequenceNumber())) {
		    source.get(activity.getKey()).setSequenceNumber(sequenceNumber);
		    sequenceNumber += 1;
		}
	    }

	    allNumbered = true;
	    for (final Entry<Integer, Activity> activity : activityList) {
		if (null == source.get(activity.getKey()).getSequenceNumber()) {
		    allNumbered = false;
		}
	    }
	}
    }

    // public Activity get(String activityName) {
    // return source.get(activityName.hashCode());
    // }

    /*
     * Earliest Activity Start Time Algorithm
     * 
     * Step 1: Let E(0) = 0.
     * 
     * Step 2: For j = 1,2,3,...,n (where n is the last activity), let ES(j) =
     * maximum {EF(i)} where the maximum is computed over all activities (i)
     * that have j as their successor.
     * 
     * Step 3: EF(j) = ES(j) + Dj
     */
    public void earliestLikelyStart() throws NullPointerException {

	if (!allNumbered) {
	    activityNumbering();
	}

	for (final Activity activity : Activity.asSortedList(source.values())) {
	    final double r0[] = new double[activity.getPredecessorList().size() + 1];
	    int r1 = 0;
	    r0[0] = 0;
	    for (final Integer pred : activity.getPredecessorList()) {
		if (null == get(pred)) {
		    throw new NullPointerException("Predecessor " + pred + " of " + activity.getActivityName() + " is null");
		}
		r0[r1++] = get(pred).getEarliestTime() + get(Integer.valueOf(pred)).getDuration().getLikely();
	    }
	    Arrays.sort(r0);
	    activity.setEarliestTime(r0[r0.length - 1]);
	}
    }

    /*
     * Latest Activity Start Time Algorithm
     * 
     * Step 1: Let L(n) equal the required completion time of the project. Note:
     * L(n) must equal or exceed E(n).
     * 
     * Step 2: For i = n-1, n-2, ..., 0, let LF(i) = minimum {LS(j)} where the
     * minimum is computed over all activities (j) that have i as their
     * predecessor.
     * 
     * Step 3: LS(i) = LF(i) - Di
     */
    public void latestLikelyStart() {

	earliestLikelyStart();

	makeEdges();

	final List<Activity> activityList = Activity.asSortedList(source.values(), false);
	final Activity last = activityList.remove(0);
	get(last.getId()).setLatestTime(last.getEarliestTime());
	logger.info(last.getActivityName() + " id: " + last.getId() + " earliest: " + last.getEarliestTime());

	for (final Activity activity : activityList) {
	    final ArrayList<Integer> successors = getSuccessor(activity);
	    if (0 < successors.size()) {
		final double r0[] = new double[successors.size()];
		int r1 = 0;
		r0[0] = 0;
		for (final Integer succ : successors) {
		    r0[r1++] = get(succ).getLatestTime() - activity.getDuration().getLikely();
		}
		Arrays.sort(r0);
		activity.setLatestTime(r0[0]);
	    }
	}
    }

    /*
     * Calculate network edges
     */
    public void makeEdges() {

	if (!allNumbered) {
	    activityNumbering();
	}

	for (final Activity activity : Activity.asSortedList(source.values())) {
	    if (0 == activity.getPredecessorList().size()) {
		activity.setEdge(0, activity.getSequenceNumber());
	    } else {
		for (final Integer pred : activity.getPredecessorList()) {
		    activity.setEdge(get(pred).getSequenceNumber(), activity.getSequenceNumber());
		}
	    }
	}
    }

    public void resetActivityNumbering() {
	for (final Activity activity : source.values()) {
	    activity.setSequenceNumber(null);
	}
	allNumbered = false;
    }

    public void simulateAll(int rounds) {

	if (!allNumbered) {
	    activityNumbering();
	}

	final List<Activity> activityList = Activity.asSortedList(source.values());

	for (final Activity activity : activityList) {
	    simulateOne(activity, rounds);
	}
    }

    public void simulateOne(Activity activity, int rounds) {

	if (!allNumbered) {
	    activityNumbering();
	}

	final ArrayList<Double> estimate = new ArrayList<Double>();
	Duration duration;
//	final List<Activity> activityList = Activity.asSortedList(source.values());

	for (int i = 0; i < rounds; i++) {
	    duration = new Duration.DurationBuilder() //
		    .likely(activity.getDurationLikely()) //
		    .worst(activity.getDurationWorst()) //
		    .best(activity.getDurationBest()) //
		    .random(0) //
		    .build();

	    estimate.add(duration.getEstimate());
	}

	final Double[] d0 = new Double[estimate.size()];

	activity.setDurationEstimate(mean(estimate.toArray(d0)));

	logger.info(activity.getActivityName() //
		+ ": expected " //
		+ ((activity.getDurationLikely() //
			+ activity.getDurationWorst() //
		+ activity.getDurationBest()) / 3) //
		+ ", estimate: " //
		+ activity.getDurationEstimate());
    }

    private ArrayList<Integer> getSuccessor(Activity theActivity) {
	final ArrayList<Integer> successors = new ArrayList<Integer>();
	final int sequence = theActivity.getSequenceNumber();
	for (final Activity activity : source.values()) {
	    for (final Edge edge : activity.getEdges()) {
		if (sequence == edge.getStart()) {
		    successors.add(activity.getId());
		}
	    }
	}
	return successors;
    }
}
